package com.kexio.user.service.impl;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import com.kexio.auth.config.SecurityProperties;
import com.kexio.auth.config.TenantModeProperties;
import com.kexio.auth.context.TenantContextHolder;
import com.kexio.auth.dto.LoginRequest;
import com.kexio.auth.dto.LoginResponse;
import com.kexio.auth.dto.MenuInfo;
import com.kexio.auth.dto.UserAuthInfo;
import com.kexio.auth.util.AuthContextUtils;
import com.kexio.auth.util.IpUtils;
import com.kexio.common.dto.SessionInfo;
import com.kexio.common.exception.BusinessException;
import com.kexio.common.utils.ValidationUtils;
import com.kexio.user.dto.LogoutRequest;
import com.kexio.user.dto.OnlineUserDetailDTO;
import com.kexio.user.dto.RefreshTokenRequest;
import com.kexio.user.dto.UserDTO;
import com.kexio.user.entity.Dept;
import com.kexio.user.entity.LoginLog;
import com.kexio.user.entity.Menu;
import com.kexio.user.entity.Role;
import com.kexio.user.entity.User;
import com.kexio.user.mapper.DeptMapper;
import com.kexio.user.mapper.LoginLogMapper;
import com.kexio.user.service.AuthService;
import com.kexio.user.service.EmailVerificationService;
import com.kexio.user.service.MenuService;
import com.kexio.user.service.UserService;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import jakarta.servlet.http.HttpServletRequest;

/**
 * 认证服务实现类
 * 
 * 提供用户认证相关的核心业务逻辑实现，包括：
 * - 用户登录认证与Sa-Token令牌生成
 * - 令牌刷新与验证
 * - 用户登出与会话管理
 * - 强制登出与批量操作
 * - 在线用户状态管理
 * 
 * 实现特性：
 * - 支持多种登录方式（用户名/手机号/邮箱）
 * - Sa-Token令牌管理（统一Token机制）
 * - 会话跟踪与管理
 * - 安全检查（账户锁定、密码错误限制）
 * - 设备和IP绑定（可选）
 * 
 * @author Kexio Team
 * @since 1.0.0
 */
@Service
public class AuthServiceImpl implements AuthService {
    
    private static final Logger logger = LoggerFactory.getLogger(AuthServiceImpl.class);
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private MenuService menuService;
    
    @Autowired
    private com.kexio.user.mapper.RoleMapper roleMapper;
    
    @Autowired
    private DeptMapper deptMapper;
    
    // ✅ Sa-Token架构：使用StpUtil进行令牌管理
    // ✅ Sa-Token架构：使用StpUtil进行Session管理
    // ✅ Sa-Token架构：缓存策略与Redis集成
    // ✅ Sa-Token架构：使用StpUtil.kickout()实现Token撤销
    
    @Autowired
    private SecurityProperties securityProperties;
    
    @Autowired
    private TenantModeProperties tenantModeProperties;

    // ✅ Sa-Token架构：安全指标监控（可选，支持Noop模式）
    @Autowired(required = false)
    private com.kexio.auth.metrics.UnifiedSecurityMetrics securityMetrics;
    
    @Autowired(required = false)
    private LoginLogMapper loginLogMapper;
    
    @Autowired(required = false)
    private EmailVerificationService emailVerificationService;

    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public LoginResponse login(LoginRequest loginRequest, HttpServletRequest request) {
        ValidationUtils.notNull(loginRequest, "登录请求不能为空");
        
        String loginAccount = loginRequest.getUsername();
        String password = loginRequest.getPassword();
        String tenantId = loginRequest.getTenantId();
        
        // 基础字段验证
        ValidationUtils.notBlank(loginAccount, "登录账号不能为空");
        ValidationUtils.notBlank(password, "密码不能为空");
        
        // 租户处理
        if (securityProperties.getTenant().isEnabled()) {
            // 租户功能启用时的处理
            if (!StringUtils.hasText(tenantId)) {
                if (securityProperties.getTenant().isAllowDefaultFallback()) {
                    tenantId = "default";
                    loginRequest.setTenantId(tenantId);
                    logger.info("租户模式启用但未提供租户ID，按配置回退 default");
                } else {
                    logger.warn("租户模式启用但未提供租户ID，且禁止默认回退");
                    throw BusinessException.authFailed("缺少租户标识");
                }
            }
        } else {
            // 租户功能关闭时，不使用租户ID
            tenantId = null;
            loginRequest.setTenantId(null);
            logger.debug("租户模式已关闭，tenantId设为null（无租户隔离）");
        }
        
        try {
            logger.info("用户尝试登录: loginAccount={}, tenantId={}, ip={}", 
                       loginAccount, tenantId, IpUtils.getClientIp(request));
            
            // 1. 认证用户
            User user = userService.authenticate(loginAccount, password, tenantId);
            if (user == null) {
                logger.warn("用户登录失败: loginAccount={}, tenantId={}", loginAccount, tenantId);
                throw BusinessException.authFailed("用户名或密码错误");
            }
            
            // 2. 检查用户状态
            validateUserStatus(user);
            
            // 3. 获取用户权限信息
            UserAuthInfo userAuthInfo = userService.getUserAuthInfoById(user.getId());
            if (userAuthInfo == null) {
                throw BusinessException.authFailed("无法获取用户权限信息");
            }
            
            // 4. 并发会话控制
            if (isConcurrentControlEnabled()) {
                try {
                    kickoutPreviousSession(tenantId, user.getId());
                } catch (RuntimeException e) {
                    logger.warn("踢出旧会话失败（不影响登录）: {}", e.getMessage());
                }
            }

            // 5. Sa-Token登录并生成Token
            String compositeLoginId = buildCompositeLoginId(tenantId, user.getId());
            logger.info("🔑 准备Sa-Token登录: compositeLoginId={}, tenantMode={}", 
                       compositeLoginId, tenantModeProperties != null ? tenantModeProperties.isEnabled() : "null");
            StpUtil.login(compositeLoginId);
            logger.info("🔑 Sa-Token登录完成: actualLoginId={}, currentLoginId={}", 
                       compositeLoginId, StpUtil.getLoginId());
            
            // 5.1 将用户权限信息存储到Sa-Token Session
            logger.info("📊 设置Session数据: sessionId={}", StpUtil.getSession().getId());
            StpUtil.getSession().set("userInfo", userAuthInfo);
            
            // 5.2 记录登录时的客户端信息
            String clientIp = IpUtils.getClientIp(request);
            String userAgent = request != null ? request.getHeader("User-Agent") : null;
            String loginLocation = getLocationByIp(clientIp);
            
            StpUtil.getSession().set("clientIp", clientIp);
            StpUtil.getSession().set("loginLocation", loginLocation);
            if (userAgent != null) {
                StpUtil.getSession().set("userAgent", userAgent);
            }
            
            // Session使用ConcurrentHashMap存储，不支持null值
            if (tenantId != null) {
                StpUtil.getSession().set("tenantId", tenantId);
                logger.info("📊 Session数据已设置: userId={}, tenantId={}, clientIp={}, sessionKeys={}", 
                           user.getId(), tenantId, clientIp, StpUtil.getSession().keys());
            } else {
                // 租户功能关闭时，不设置tenantId键（保持Session中没有这个key）
                logger.info("📊 Session数据已设置: userId={}, tenantId=null（租户功能关闭），clientIp={}, sessionKeys={}", 
                           user.getId(), clientIp, StpUtil.getSession().keys());
            }
            
            // 5.2 同步设置租户信息到TenantContextHolder（确保与MyBatis拦截器一致）
            TenantContextHolder.setCurrentTenantId(tenantId);
            logger.info("🏢 租户上下文已设置: tenantId={}", tenantId);
            
            // 5.3 获取Sa-Token生成的Token值
            String accessToken = StpUtil.getTokenValue();
            logger.info("🎯 生成访问令牌: token={}, sessionId={}, isLogin={}", 
                       accessToken.substring(0, Math.min(accessToken.length(), 8)) + "...", 
                       StpUtil.getSession().getId(), StpUtil.isLogin());
            
            logger.info("✅ Sa-Token登录完成: userId={}, tenantId={}, permCount={}, roleCount={}, newToken={}", 
                       user.getId(), tenantId, 
                       userAuthInfo.getPermissions() != null ? userAuthInfo.getPermissions().size() : 0,
                       userAuthInfo.getRoles() != null ? userAuthInfo.getRoles().size() : 0,
                       accessToken.substring(0, Math.min(accessToken.length(), 8)) + "..."); // 只显示前8位
            
            // 6. 更新用户最后登录信息
            userService.updateLastLoginTime(user.getId(), LocalDateTime.now());
            
            // 8. 构建登录响应
            LoginResponse response = new LoginResponse();
            response.setAccessToken(accessToken);
            response.setRefreshToken(accessToken); // Sa-Token使用同一Token
            response.setTokenType("Bearer");
            response.setExpiresIn((long)StpUtil.getTokenTimeout());
            response.setRefreshExpiresIn((long)StpUtil.getTokenTimeout());
            response.setSessionId(StpUtil.getTokenValue()); // 使用token作为sessionId
            
            // 设置用户信息
            LoginResponse.UserInfo userInfo = new LoginResponse.UserInfo();
            userInfo.setUserId(user.getId());
            userInfo.setTenantId(tenantId);
            userInfo.setUsername(user.getUsername());
            userInfo.setNickname(user.getNickname());
            userInfo.setAvatar(user.getAvatarUrl());
            userInfo.setDeptId(user.getDeptId());
            userInfo.setStatus(user.getStatus());
            response.setUserInfo(userInfo);
            
            // 设置权限信息
            LoginResponse.AuthInfo authInfo = new LoginResponse.AuthInfo();
            authInfo.setRoles(userAuthInfo.getRoles());
            authInfo.setPermissions(userAuthInfo.getPermissions());
            
            // ✅ 直接返回树形菜单（一次查询，前端可直接使用）
            logger.info("========================================");
            logger.info("【登录菜单-开始】userId={}, tenantId={}", user.getId(), tenantId);
            logger.info("========================================");
            
            List<Menu> menuTree = menuService.getUserMenuTree(user.getId(), tenantId);
            logger.info("【登录菜单】MenuService返回结果: menuTree={}", menuTree != null ? menuTree.size() + "个根菜单" : "null");
            
            if (menuTree != null && !menuTree.isEmpty()) {
                logger.info("【登录菜单】第一个菜单: id={}, name={}, title={}", 
                    menuTree.get(0).getId(), menuTree.get(0).getName(), menuTree.get(0).getTitle());
            }
            
            List<MenuInfo> menuInfoTree = convertMenuTreeToMenuInfo(menuTree);
            logger.info("【登录菜单】转换后DTO数量: {}", menuInfoTree != null ? menuInfoTree.size() : 0);
            
            authInfo.setMenus(menuInfoTree);
            logger.info("【登录菜单】设置到authInfo: menuCount={}, totalMenus={}", 
                menuInfoTree != null ? menuInfoTree.size() : 0, 
                countTotalMenus(menuInfoTree));
            logger.info("========================================");
            
            authInfo.setDataScope(userAuthInfo.getDataScope());
            response.setAuthInfo(authInfo);
            
            // 设置登录时间和IP
            response.setLoginTime(LocalDateTime.now());
            response.setLoginIp(IpUtils.getClientIp(request));
            
            logger.info("用户登录成功: userId={}, username={}, tenantId={}", 
                       user.getId(), user.getUsername(), tenantId);
            
            // 记录登录日志
            try {
                if (loginLogMapper != null) {
                    LoginLog log = new LoginLog();
                    log.setTenantId(tenantId);
                    log.setUsername(user.getUsername());
                    log.setIp(IpUtils.getClientIp(request));
                    log.setLocation(null);
                    log.setUserAgent(request != null ? request.getHeader("User-Agent") : null);
                    log.setStatus("SUCCESS");
                    log.setMessage("登录成功");
                    log.setCreateTime(LocalDateTime.now());
                    loginLogMapper.insert(log);
                }
            } catch (Exception e) {
                logger.debug("记录登录成功日志失败: {}", e.getMessage());
            }
            
            return response;
            
        } catch (BusinessException e) {
            logger.warn("用户登录失败: loginAccount={}, tenantId={}, error={}", loginAccount, tenantId, e.getMessage());
            try {
                if (loginLogMapper != null) {
                    LoginLog log = new LoginLog();
                    log.setTenantId(StringUtils.hasText(tenantId) ? tenantId : "default");
                    log.setUsername(loginAccount);
                    log.setIp(IpUtils.getClientIp(request));
                    log.setUserAgent(request != null ? request.getHeader("User-Agent") : null);
                    log.setStatus("FAILURE");
                    log.setMessage(e.getMessage());
                    log.setCreateTime(LocalDateTime.now());
                    loginLogMapper.insert(log);
                }
            } catch (Exception ex) {
                logger.debug("记录登录失败日志失败: {}", ex.getMessage());
            }
            throw e;
        } catch (Exception e) {
            logger.error("用户登录异常: loginAccount={}, tenantId={}, error={}", 
                        loginAccount, tenantId, e.getMessage(), e);
            try {
                if (loginLogMapper != null) {
                    LoginLog log = new LoginLog();
                    log.setTenantId(tenantId);
                    log.setUsername(loginAccount);
                    log.setIp(IpUtils.getClientIp(request));
                    log.setUserAgent(request != null ? request.getHeader("User-Agent") : null);
                    log.setStatus("FAILURE");
                    log.setMessage("exception");
                    log.setCreateTime(LocalDateTime.now());
                    loginLogMapper.insert(log);
                }
            } catch (Exception ex) {
                logger.debug("记录登录异常日志失败: {}", ex.getMessage());
            }
            throw BusinessException.authFailed("登录失败，请稍后再试");
        }
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public LoginResponse refreshToken(RefreshTokenRequest refreshRequest, HttpServletRequest request) {
        if (refreshRequest == null || !StringUtils.hasText(refreshRequest.getRefreshToken())) {
            throw new IllegalArgumentException("刷新令牌不能为空");
        }
        
        try {
            String refreshToken = refreshRequest.getRefreshToken();
            
            // 1. 检查Token有效性
            if (!StpUtil.isLogin()) {
                throw new NotLoginException("用户未登录或会话已过期", StpUtil.getLoginType(), NotLoginException.NOT_TOKEN);
            }
            
            // 2. 获取当前用户信息
            String userId = StpUtil.getLoginIdAsString();
            String tenantId = (String) StpUtil.getSession().get("tenantId");
            
            if (!StringUtils.hasText(userId) || !StringUtils.hasText(tenantId)) {
                throw new NotLoginException("会话中缺少用户信息", StpUtil.getLoginType(), NotLoginException.INVALID_TOKEN);
            }
            
            // 3. 获取最新的用户权限信息
            UserAuthInfo userAuthInfo = userService.getUserAuthInfoById(userId);
            if (userAuthInfo == null || !userAuthInfo.isValid()) {
                throw new NotLoginException("用户信息不存在或已被禁用", StpUtil.getLoginType(), NotLoginException.INVALID_TOKEN);
            }
            
            // 4. Sa-Token: 续期Token
            StpUtil.renewTimeout(StpUtil.getTokenTimeout());
            
            // 4.1 更新Session中的用户权限信息
            StpUtil.getSession().set("userInfo", userAuthInfo);
            
            // 5. 获取续期后的Token
            String newAccessToken = StpUtil.getTokenValue();
            
            // 6. 构建刷新响应
            LoginResponse response = new LoginResponse();
            response.setAccessToken(newAccessToken);
            response.setRefreshToken(newAccessToken); // Sa-Token使用同一Token
            response.setTokenType("Bearer");
            response.setExpiresIn((long)StpUtil.getTokenTimeout());
            response.setRefreshExpiresIn((long)StpUtil.getTokenTimeout());
            
            // 设置用户信息
            User user = userService.getUserById(userId);
            if (user != null) {
                LoginResponse.UserInfo userInfo = new LoginResponse.UserInfo();
                userInfo.setUserId(user.getId());
                userInfo.setTenantId(tenantId);
                userInfo.setUsername(user.getUsername());
                userInfo.setNickname(user.getNickname());
                userInfo.setAvatar(user.getAvatarUrl());
                userInfo.setDeptId(user.getDeptId());
                userInfo.setStatus(user.getStatus());
                response.setUserInfo(userInfo);
            }
            
            // 设置权限信息
            LoginResponse.AuthInfo authInfo = new LoginResponse.AuthInfo();
            authInfo.setRoles(userAuthInfo.getRoles());
            authInfo.setPermissions(userAuthInfo.getPermissions());
            
            // ✅ 直接返回树形菜单（一次查询，前端可直接使用）
            List<Menu> menuTree = menuService.getUserMenuTree(userId, tenantId);
            List<MenuInfo> menuInfoTree = convertMenuTreeToMenuInfo(menuTree);
            authInfo.setMenus(menuInfoTree);
            
            authInfo.setDataScope(userAuthInfo.getDataScope());
            response.setAuthInfo(authInfo);
            
            logger.info("Token刷新成功: userId={}, tenantId={}", userId, tenantId);
            return response;
            
        } catch (BusinessException e) {
            logger.warn("Token刷新失败: error={}", e.getMessage());
            throw e;
        } catch (Exception e) {
            logger.error("Token刷新异常: error={}", e.getMessage(), e);
            throw BusinessException.authFailed("Token刷新失败");
        }
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean logout(LogoutRequest logoutRequest, HttpServletRequest request) {
        try {
            // Sa-Token登出
            if (StpUtil.isLogin()) {
                String userId = StpUtil.getLoginIdAsString();
                String tenantId = (String) StpUtil.getSession().get("tenantId");
                
                StpUtil.logout();
                logger.info("Sa-Token用户登出成功: userId={}, tenantId={}", userId, tenantId);
                return true;
            } else {
                logger.info("用户未登录，登出操作跳过");
                return true;
            }
        } catch (Exception e) {
            logger.error("登出异常: error={}", e.getMessage(), e);
            return false;
        }
    }
    
    @Override
    public UserDTO getCurrentUserProfile() {
        try {
            // 从安全上下文中获取当前用户信息
            UserAuthInfo currentUser = AuthContextUtils.getCurrentUser();
            if (currentUser == null) {
                throw new NotLoginException("未找到当前用户信息，请重新登录", StpUtil.getLoginType(), NotLoginException.NOT_TOKEN);
            }
            
            // 查询最新的用户信息
            User user = userService.getUserById(currentUser.getUserId());
            if (user == null || !user.isValid()) {
                throw new NotLoginException("用户不存在或已被禁用", StpUtil.getLoginType(), NotLoginException.INVALID_TOKEN);
            }
            
            // 转换为DTO
            UserDTO userDTO = new UserDTO();
            userDTO.setUserId(user.getId());
            // 使用认证上下文中的租户ID
            userDTO.setTenantId(AuthContextUtils.getCurrentTenantId());
            userDTO.setUsername(user.getUsername());
            userDTO.setNickname(user.getNickname());
            userDTO.setPhone(user.getPhone());
            userDTO.setEmail(user.getEmail());
            userDTO.setAvatarUrl(user.getAvatarUrl());
            userDTO.setDeptId(user.getDeptId());
            userDTO.setStatus(user.getStatus());
            userDTO.setLastLoginTime(user.getLastLoginTime());
            userDTO.setCreateTime(user.getCreateTime());
            
            // ✅ 设置权限版本号（用于前端缓存控制）
            userDTO.setPermVersion(user.getPermVersion());
            
            // 设置权限信息
            userDTO.setRoles(currentUser.getRoles());
            // UserDTO没有setPermissions方法，权限信息通过其他方式传递
            
            return userDTO;
            
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            logger.error("获取当前用户信息异常: error={}", e.getMessage(), e);
            throw BusinessException.authFailed("获取用户信息失败，请稍后再试");
        }
    }
    
    @Override
    public boolean validateAccessToken(String accessToken, String tenantId) {
        try {
            // Sa-Token验证
            if (StpUtil.isLogin() && accessToken.equals(StpUtil.getTokenValue())) {
                // 验证租户匹配
                if (StringUtils.hasText(tenantId)) {
                    String tokenTenantId = (String) StpUtil.getSession().get("tenantId");
                    return tenantId.equals(tokenTenantId);
                }
                return true;
            }
            return false;
        } catch (Exception e) {
            logger.debug("Sa-Token访问令牌验证异常: {}", e.getMessage());
            return false;
        }
    }
    
    @Override
    public boolean validateRefreshToken(String refreshToken, String tenantId) {
        try {
            // Sa-Token验证（Sa-Token中refresh和access是同一个token）
            if (StpUtil.isLogin() && refreshToken.equals(StpUtil.getTokenValue())) {
                // 验证租户匹配
                if (StringUtils.hasText(tenantId)) {
                    String tokenTenantId = (String) StpUtil.getSession().get("tenantId");
                    return tenantId.equals(tokenTenantId);
                }
                return true;
            }
            return false;
        } catch (Exception e) {
            logger.debug("Sa-Token刷新令牌验证异常: {}", e.getMessage());
            return false;
        }
    }
    
    @Override
    public boolean forceLogout(String userId, String operatorId, String reason) {
        if (!StringUtils.hasText(userId)) {
            logger.warn("强制登出失败: userId为空");
            return false;
        }
        
        String tenantId = AuthContextUtils.getCurrentTenantId();
        if (!StringUtils.hasText(tenantId)) {
            logger.warn("无法获取当前租户ID，强制登出失败: userId={}", userId);
            return false;
        }
        
        try {
            String complexLoginId = tenantId + ":" + userId;
            
            if (!StpUtil.isLogin(complexLoginId)) {
                logger.info("用户未登录，无需强制登出: userId={}, tenantId={}", userId, tenantId);
                return true;
            }
            
            StpUtil.logout(complexLoginId);
            
            logger.warn("强制登出成功: userId={}, tenantId={}, operatorId={}, reason={}", 
                       userId, tenantId, operatorId, reason);
            
            return true;
        } catch (Exception e) {
            logger.error("强制登出失败: userId={}, tenantId={}, operatorId={}, reason={}, error={}", 
                        userId, tenantId, operatorId, reason, e.getMessage(), e);
            return false;
        }
    }
    
    
    @Override
    public int batchForceLogout(List<String> userIds, String operatorId, String reason) {
        if (userIds == null || userIds.isEmpty()) {
            return 0;
        }
        
        String tenantId = AuthContextUtils.getCurrentTenantId();
        if (!StringUtils.hasText(tenantId)) {
            logger.warn("无法获取当前租户ID，批量强制登出失败");
            return 0;
        }
        
        int successCount = 0;
        
        for (String userId : userIds) {
            try {
                boolean success = forceLogout(userId, operatorId, reason);
                if (success) {
                    successCount++;
                }
            } catch (Exception e) {
                logger.error("批量强制登出单个用户失败: userId={}, tenantId={}", userId, tenantId, e);
            }
        }
        
        logger.warn("批量强制登出完成: total={}, success={}, tenantId={}, operatorId={}, reason={}", 
                   userIds.size(), successCount, tenantId, operatorId, reason);
        
        return successCount;
    }
    
    
    
    
    @Override
    public boolean isUserOnline(String userId) {
        if (!StringUtils.hasText(userId)) {
            return false;
        }
        
        String tenantId = AuthContextUtils.getCurrentTenantId();
        if (!StringUtils.hasText(tenantId)) {
            logger.warn("无法获取当前租户ID，检查用户在线状态失败: userId={}", userId);
            return false;
        }
        
        try {
            // ✨ 现代架构: 通过Sa-Token检查用户在线状态
            // Sa-Token架构下在线状态检查已简化
            return StpUtil.isLogin(userId);
        } catch (Exception e) {
            logger.error("检查用户在线状态失败: userId={}, tenantId={}, error={}", userId, tenantId, e.getMessage());
            return false;
        }
    }
    
    @Override
    public long getOnlineUserCount(String tenantId) {
        try {
            List<SessionInfo> sessions = getTenantActiveSessions(tenantId);
            long count = sessions != null ? sessions.size() : 0L;
            logger.debug("获取在线用户数量: tenantId={}, count={}", tenantId, count);
            return count;
        } catch (Exception e) {
            logger.error("获取在线用户数量失败: tenantId={}, error={}", tenantId, e.getMessage());
            return 0L;
        }
    }
    
    @Override
    public OnlineUserDetailDTO getOnlineUserDetails(String tenantId, boolean includeDetails) {
        try {
            List<SessionInfo> sessions = getTenantActiveSessions(tenantId);
            
            OnlineUserDetailDTO result = new OnlineUserDetailDTO();
            OnlineUserDetailDTO.OnlineStatistics statistics = buildStatistics(sessions);
            result.setStatistics(statistics);
            
            if (includeDetails) {
                List<OnlineUserDetailDTO.OnlineUserInfo> users = buildOnlineUserInfoList(sessions);
                result.setUsers(users);
                result.setSessionDistribution(buildSessionDistribution(users));
            }
            
            logger.debug("获取在线用户详细信息成功: tenantId={}, 总数={}", tenantId, statistics.getTotalCount());
            return result;
            
        } catch (Exception e) {
            logger.error("获取在线用户详细信息失败: tenantId={}, error={}", tenantId, e.getMessage(), e);
            return createEmptyOnlineUserDetail();
        }
    }
    
    private OnlineUserDetailDTO createEmptyOnlineUserDetail() {
        OnlineUserDetailDTO emptyResult = new OnlineUserDetailDTO();
        OnlineUserDetailDTO.OnlineStatistics emptyStats = new OnlineUserDetailDTO.OnlineStatistics();
        emptyStats.setTotalCount(0L);
        emptyStats.setActiveCount(0L);
        emptyStats.setIdleCount(0L);
        emptyStats.setTodayLoginCount(0L);
        emptyStats.setAverageOnlineMinutes(0.0);
        emptyStats.setStatisticTime(LocalDateTime.now());
        emptyResult.setStatistics(emptyStats);
        return emptyResult;
    }
    
    /**
     * 构建统计信息
     */
    private OnlineUserDetailDTO.OnlineStatistics buildStatistics(List<SessionInfo> sessions) {
        OnlineUserDetailDTO.OnlineStatistics stats = new OnlineUserDetailDTO.OnlineStatistics();
        LocalDateTime now = LocalDateTime.now();
        
        stats.setTotalCount((long) sessions.size());
        stats.setStatisticTime(now);
        
        if (sessions.isEmpty()) {
            stats.setActiveCount(0L);
            stats.setIdleCount(0L);
            stats.setTodayLoginCount(0L);
            stats.setAverageOnlineMinutes(0.0);
            return stats;
        }
        
        LocalDateTime startOfToday = now.toLocalDate().atStartOfDay();
        LocalDateTime activeThreshold = now.minusMinutes(5);
        
        long activeCount = 0;
        long todayLoginCount = 0;
        double totalOnlineMinutes = 0;
        
        for (SessionInfo session : sessions) {
            if (session.getLastActiveTime() != null && session.getLastActiveTime().isAfter(activeThreshold)) {
                activeCount++;
            }
            
            if (session.getCreateTime() != null) {
                if (session.getCreateTime().isAfter(startOfToday)) {
                    todayLoginCount++;
                }
                totalOnlineMinutes += java.time.Duration.between(session.getCreateTime(), now).toMinutes();
            }
        }
        
        stats.setActiveCount(activeCount);
        stats.setIdleCount(stats.getTotalCount() - activeCount);
        stats.setTodayLoginCount(todayLoginCount);
        stats.setAverageOnlineMinutes(totalOnlineMinutes / sessions.size());
        
        return stats;
    }
    
    /**
     * 构建在线用户信息列表
     */
    private List<OnlineUserDetailDTO.OnlineUserInfo> buildOnlineUserInfoList(List<SessionInfo> sessions) {
        List<OnlineUserDetailDTO.OnlineUserInfo> users = new ArrayList<>();
        
        for (SessionInfo session : sessions) {
            OnlineUserDetailDTO.OnlineUserInfo userInfo = new OnlineUserDetailDTO.OnlineUserInfo();
            
            // 基础会话信息
            userInfo.setUserId(session.getUserId());
            userInfo.setSessionId(session.getSessionId());
            userInfo.setTenantId(session.getTenantId());
            userInfo.setLoginType(session.getLoginType() != null ? session.getLoginType() : "web");
            
            // 时间信息
            userInfo.setLoginTime(session.getCreateTime());
            userInfo.setLastActiveTime(session.getLastActiveTime());
            
            // 计算在线时长
            if (session.getCreateTime() != null) {
                long minutes = java.time.Duration.between(session.getCreateTime(), LocalDateTime.now()).toMinutes();
                userInfo.setOnlineMinutes(Math.max(0, minutes));
            }
            
            // 计算剩余时间
            if (session.getTokenTimeout() != null && session.getTokenTimeout() > 0) {
                userInfo.setRemainingSeconds(session.getTokenTimeout());
            }
            
            // 活跃状态判断
            LocalDateTime fiveMinutesAgo = LocalDateTime.now().minusMinutes(5);
            boolean isActive = session.getLastActiveTime() != null && 
                              session.getLastActiveTime().isAfter(fiveMinutesAgo);
            userInfo.setIsActive(isActive);
            
            // 客户端信息
            userInfo.setClientIp(session.getClientIp());
            userInfo.setLoginLocation(session.getLoginLocation());
            userInfo.setUserAgent(session.getUserAgent());
            userInfo.setDeviceInfo(session.getDeviceInfo());
            
            // 解析浏览器和设备信息
            parseUserAgentInfo(userInfo, session.getUserAgent());
            
            // 获取用户详细信息
            enrichUserInfo(userInfo, session.getUserId());
            
            users.add(userInfo);
        }
        
        return users;
    }
    
    /**
     * 解析用户代理信息
     */
    private void parseUserAgentInfo(OnlineUserDetailDTO.OnlineUserInfo userInfo, String userAgent) {
        if (userAgent == null || userAgent.isEmpty()) {
            setDefaultUserAgentInfo(userInfo);
            return;
        }
        
        String ua = userAgent.toLowerCase();
        userInfo.setBrowser(detectBrowser(ua));
        userInfo.setOs(detectOS(ua));
        userInfo.setDeviceType(detectDeviceType(ua));
    }
    
    private void setDefaultUserAgentInfo(OnlineUserDetailDTO.OnlineUserInfo userInfo) {
        userInfo.setBrowser("Unknown");
        userInfo.setOs("Unknown");
        userInfo.setDeviceType("unknown");
    }
    
    private String detectBrowser(String ua) {
        if (ua.contains("edg")) return "Edge";
        if (ua.contains("chrome")) return "Chrome";
        if (ua.contains("firefox")) return "Firefox";
        if (ua.contains("safari")) return "Safari";
        if (ua.contains("msie") || ua.contains("trident")) return "Internet Explorer";
        return "Other";
    }
    
    private String detectOS(String ua) {
        if (ua.contains("windows")) return "Windows";
        if (ua.contains("android")) return "Android";
        if (ua.contains("iphone") || ua.contains("ipad")) return "iOS";
        if (ua.contains("mac")) return "macOS";
        if (ua.contains("linux")) return "Linux";
        return "Unknown";
    }
    
    private String detectDeviceType(String ua) {
        if (ua.contains("tablet") || ua.contains("ipad")) return "tablet";
        if (ua.contains("mobile") || ua.contains("android") || ua.contains("iphone")) return "mobile";
        return "desktop";
    }
    
    /**
     * 丰富用户信息
     */
    private void enrichUserInfo(OnlineUserDetailDTO.OnlineUserInfo userInfo, String userId) {
        try {
            User user = userService.getUserById(userId);
            if (user == null) {
                setDefaultUserInfo(userInfo, userId, "未知用户");
                logger.warn("用户不存在: userId={}", userId);
                return;
            }
            
            userInfo.setUsername(user.getUsername());
            userInfo.setNickname(user.getNickname() != null ? user.getNickname() : user.getUsername());
            userInfo.setDeptName(getDeptName(user.getDeptId()));
            userInfo.setRoleNames(getUserRoleNames(userId));
            
        } catch (Exception e) {
            logger.error("获取用户详细信息失败: userId={}, error={}", userId, e.getMessage());
            setDefaultUserInfo(userInfo, userId, "错误");
        }
    }
    
    private void setDefaultUserInfo(OnlineUserDetailDTO.OnlineUserInfo userInfo, String userId, String prefix) {
        userInfo.setUsername(prefix + "-" + userId);
        userInfo.setNickname(prefix + "-" + userId);
        userInfo.setDeptName("未知部门");
        userInfo.setRoleNames(java.util.Arrays.asList("无角色"));
    }
    
    private String getDeptName(String deptId) {
        if (deptId == null) {
            return "未分配部门";
        }
        
        try {
            Dept dept = deptMapper.selectById(deptId);
            return dept != null ? dept.getDeptName() : "未知部门";
        } catch (Exception e) {
            logger.debug("查询部门信息失败: deptId={}", deptId);
            return "未知部门";
        }
    }
    
    private List<String> getUserRoleNames(String userId) {
        try {
            List<String> roleIds = userService.getUserRoles(userId);
            if (roleIds == null || roleIds.isEmpty()) {
                return java.util.Arrays.asList("无角色");
            }
            
            List<Role> roles = roleMapper.selectBatchIds(roleIds);
            if (roles.isEmpty()) {
                return java.util.Arrays.asList("无角色");
            }
            
            return roles.stream()
                    .map(Role::getName)
                    .collect(java.util.stream.Collectors.toList());
                    
        } catch (Exception e) {
            logger.debug("查询用户角色失败: userId={}", userId);
            return java.util.Arrays.asList("无角色");
        }
    }
    
    /**
     * 构建会话分布统计
     */
    private OnlineUserDetailDTO.SessionDistribution buildSessionDistribution(List<OnlineUserDetailDTO.OnlineUserInfo> users) {
        OnlineUserDetailDTO.SessionDistribution distribution = new OnlineUserDetailDTO.SessionDistribution();
        
        // 设备统计
        OnlineUserDetailDTO.SessionDistribution.DeviceStats deviceStats = 
            new OnlineUserDetailDTO.SessionDistribution.DeviceStats();
        
        // 浏览器统计
        OnlineUserDetailDTO.SessionDistribution.BrowserStats browserStats = 
            new OnlineUserDetailDTO.SessionDistribution.BrowserStats();
        
        // 时间分布统计
        OnlineUserDetailDTO.SessionDistribution.TimeStats timeStats = 
            new OnlineUserDetailDTO.SessionDistribution.TimeStats();
        
        for (OnlineUserDetailDTO.OnlineUserInfo user : users) {
            switch (user.getDeviceType()) {
                case "mobile" -> deviceStats.setMobileCount(deviceStats.getMobileCount() + 1);
                case "tablet" -> deviceStats.setTabletCount(deviceStats.getTabletCount() + 1);
                case "desktop" -> deviceStats.setDesktopCount(deviceStats.getDesktopCount() + 1);
                default -> deviceStats.setUnknownCount(deviceStats.getUnknownCount() + 1);
            }
            
            switch (user.getBrowser()) {
                case "Chrome" -> browserStats.setChromeCount(browserStats.getChromeCount() + 1);
                case "Firefox" -> browserStats.setFirefoxCount(browserStats.getFirefoxCount() + 1);
                case "Safari" -> browserStats.setSafariCount(browserStats.getSafariCount() + 1);
                case "Edge" -> browserStats.setEdgeCount(browserStats.getEdgeCount() + 1);
                case "Internet Explorer" -> browserStats.setIeCount(browserStats.getIeCount() + 1);
                default -> browserStats.setOtherCount(browserStats.getOtherCount() + 1);
            }
            
            // 时间分布统计（基于登录时间）
            if (user.getLoginTime() != null) {
                int hour = user.getLoginTime().getHour();
                if (hour >= 6 && hour < 12) {
                    timeStats.setMorningCount(timeStats.getMorningCount() + 1);
                } else if (hour >= 12 && hour < 18) {
                    timeStats.setAfternoonCount(timeStats.getAfternoonCount() + 1);
                } else if (hour >= 18 && hour < 24) {
                    timeStats.setEveningCount(timeStats.getEveningCount() + 1);
                } else {
                    timeStats.setNightCount(timeStats.getNightCount() + 1);
                }
            }
        }
        
        distribution.setDeviceStats(deviceStats);
        distribution.setBrowserStats(browserStats);
        distribution.setTimeStats(timeStats);
        
        return distribution;
    }
    
    /**
     * 根据IP地址获取地理位置
     */
    private String getLocationByIp(String clientIp) {
        if (clientIp == null || "unknown".equals(clientIp)) {
            return "未知位置";
        }
        
        // 本地IP地址
        if (clientIp.startsWith("127.") || clientIp.startsWith("192.168.") || 
            clientIp.startsWith("10.") || clientIp.equals("0:0:0:0:0:0:0:1")) {
            return "本地网络";
        }
        
        // TODO: 这里可以集成第三方IP地址库或API来获取真实地理位置
        // 例如: ip2region、GeoLite2等
        // 暂时返回IP地址信息
        return "IP: " + clientIp;
    }
    
    @Override
    public List<SessionInfo> getTenantActiveSessions(String tenantId) {
        try {
            logger.info("🔍 获取租户活跃会话: targetTenantId={}", tenantId);
            
            List<SessionInfo> tenantSessions = new ArrayList<>();
            
            // 通过Redis扫描租户的所有会话key
            try {
                // 获取Sa-Token的DAO
                cn.dev33.satoken.dao.SaTokenDao saTokenDao = cn.dev33.satoken.SaManager.getSaTokenDao();
                
                // 尝试使用searchData API
                List<String> sessionKeys = null;
                try {
                    sessionKeys = saTokenDao.searchData("satoken:login:session:", "", -1, -1, false);
                    logger.debug("searchData找到 {} 个会话key", sessionKeys != null ? sessionKeys.size() : 0);
                } catch (Exception e) {
                    logger.warn("searchData API不可用: {}", e.getMessage());
                }
                
                // 如果searchData不可用，使用备用方案：查询数据库中的所有用户
                if (sessionKeys == null || sessionKeys.isEmpty()) {
                    logger.info("使用备用方案：查询数据库中的用户并检查在线状态");
                    
                    // 查询租户内的所有用户
                    List<com.kexio.user.entity.User> users = userService.getAllUsers();
                    logger.debug("数据库中有 {} 个用户", users != null ? users.size() : 0);
                    
                    if (users != null && !users.isEmpty()) {
                        for (com.kexio.user.entity.User user : users) {
                            try {
                                String userId = user.getId();
                                
                                // 检查用户是否在线
                                if (!StpUtil.isLogin(userId)) {
                                    continue;
                                }
                                
                                // 获取用户的session
                                SaSession session = StpUtil.getSessionByLoginId(userId, false);
                                if (session == null) {
                                    continue;
                                }
                                
                    // 检查租户匹配
                    Object sessionTenantId = session.has("tenantId") ? session.get("tenantId") : null;
                    
                    boolean tenantMatches = false;
                    if (tenantId == null && sessionTenantId == null) {
                        tenantMatches = true; // 租户功能关闭
                    } else if (tenantId != null && sessionTenantId != null && sessionTenantId.toString().equals(tenantId)) {
                        tenantMatches = true; // 租户ID匹配
                    }
                    
                    if (tenantMatches) {
                                    SessionInfo sessionInfo = buildSessionInfo(session, userId);
                                    if (sessionInfo != null) {
                                        tenantSessions.add(sessionInfo);
                                        logger.debug("添加在线用户: userId={}, username={}", userId, user.getUsername());
                                    }
                                }
                            } catch (Exception e) {
                                logger.debug("检查用户在线状态失败: userId={}, error={}", user.getId(), e.getMessage());
                            }
                        }
                    }
                } else {
                    // 使用searchData的结果
                    for (String sessionKey : sessionKeys) {
                        try {
                            // 从key中提取userId
                            String userId = sessionKey.substring("satoken:login:session:".length());
                            
                            // 检查用户是否仍然在线
                            if (!StpUtil.isLogin(userId)) {
                                continue;
                            }
                            
                            // 获取用户的session
                            SaSession session = StpUtil.getSessionByLoginId(userId, false);
                            if (session == null) {
                                continue;
                            }
                            
                            // 检查租户匹配
                            Object sessionTenantId = session.has("tenantId") ? session.get("tenantId") : null;
                            
                            boolean tenantMatches = false;
                            if (tenantId == null && sessionTenantId == null) {
                                tenantMatches = true; // 租户功能关闭
                            } else if (tenantId != null && sessionTenantId != null && sessionTenantId.toString().equals(tenantId)) {
                                tenantMatches = true; // 租户ID匹配
                            }
                            
                            if (tenantMatches) {
                                SessionInfo sessionInfo = buildSessionInfo(session, userId);
                                if (sessionInfo != null) {
                                    tenantSessions.add(sessionInfo);
                                }
                            }
                        } catch (Exception e) {
                            logger.debug("处理会话key失败: {}, error={}", sessionKey, e.getMessage());
                        }
                    }
                }
            } catch (Exception e) {
                logger.error("查询会话失败: {}", e.getMessage(), e);
            }
            
            logger.info("✅ 获取租户活跃会话成功: tenantId={}, sessionCount={}", tenantId, tenantSessions.size());
            return tenantSessions;
            
        } catch (Exception e) {
            logger.error("获取租户活跃会话失败: tenantId={}, error={}", tenantId, e.getMessage(), e);
            return Collections.emptyList();
        }
    }
    
    @Override
    public List<SessionInfo> getAllActiveSessions() {
        try {
            logger.info("🔍 获取全局所有活跃会话（跨租户）");
            
            List<SessionInfo> allSessions = new ArrayList<>();
            
            try {
                // 获取Sa-Token的DAO
                cn.dev33.satoken.dao.SaTokenDao saTokenDao = cn.dev33.satoken.SaManager.getSaTokenDao();
                
                // 尝试使用searchData API
                List<String> sessionKeys = null;
                try {
                    sessionKeys = saTokenDao.searchData("satoken:login:session:", "", -1, -1, false);
                    logger.debug("searchData找到 {} 个会话key（全局）", sessionKeys != null ? sessionKeys.size() : 0);
                } catch (Exception e) {
                    logger.warn("searchData API不可用: {}", e.getMessage());
                }
                
                // 如果searchData不可用，使用备用方案：查询数据库中的所有用户
                if (sessionKeys == null || sessionKeys.isEmpty()) {
                    logger.info("使用备用方案：查询数据库中的所有用户并检查在线状态");
                    
                    // 查询所有用户（不过滤租户）
                    List<com.kexio.user.entity.User> users = userService.getAllUsers();
                    logger.debug("数据库中有 {} 个用户", users != null ? users.size() : 0);
                    
                    if (users != null && !users.isEmpty()) {
                        for (com.kexio.user.entity.User user : users) {
                            try {
                                String userId = user.getId();
                                
                                // 检查用户是否在线
                                if (!StpUtil.isLogin(userId)) {
                                    continue;
                                }
                                
                                // 获取用户的session
                                SaSession session = StpUtil.getSessionByLoginId(userId, false);
                                if (session == null) {
                                    continue;
                                }
                                
                                // 全局查询不过滤租户，直接添加
                                SessionInfo sessionInfo = buildSessionInfo(session, userId);
                                if (sessionInfo != null) {
                                    allSessions.add(sessionInfo);
                                    logger.debug("添加在线用户（全局）: userId={}, username={}", userId, user.getUsername());
                                }
                            } catch (Exception e) {
                                logger.debug("检查用户在线状态失败: userId={}, error={}", user.getId(), e.getMessage());
                            }
                        }
                    }
                } else {
                    // 使用searchData的结果
                    for (String sessionKey : sessionKeys) {
                        try {
                            // 从key中提取userId
                            String userId = sessionKey.substring("satoken:login:session:".length());
                            
                            // 检查用户是否仍然在线
                            if (!StpUtil.isLogin(userId)) {
                                continue;
                            }
                            
                            // 获取用户的session
                            SaSession session = StpUtil.getSessionByLoginId(userId, false);
                            if (session == null) {
                                continue;
                            }
                            
                            // 全局查询不过滤租户，直接添加所有会话
                            SessionInfo sessionInfo = buildSessionInfo(session, userId);
                            if (sessionInfo != null) {
                                allSessions.add(sessionInfo);
                            }
                        } catch (Exception e) {
                            logger.debug("处理会话key失败: {}, error={}", sessionKey, e.getMessage());
                        }
                    }
                }
            } catch (Exception e) {
                logger.error("查询会话失败: {}", e.getMessage(), e);
            }
            
            logger.info("✅ 获取全局活跃会话成功: sessionCount={}", allSessions.size());
            return allSessions;
            
        } catch (Exception e) {
            logger.error("获取全局活跃会话失败: error={}", e.getMessage(), e);
            return Collections.emptyList();
        }
    }
    
    /**
     * 从SaSession构建SessionInfo对象
     */
    private SessionInfo buildSessionInfo(SaSession session, String userId) {
        try {
            SessionInfo sessionInfo = new SessionInfo(session.getId(), userId);
            
            // 租户信息
            if (session.has("tenantId")) {
                sessionInfo.setTenantId(String.valueOf(session.get("tenantId")));
            }
            
            // 基础信息
            sessionInfo.setLoginType(session.has("loginType") ? String.valueOf(session.get("loginType")) : "web");
            
            // 用户信息 - 优先从session获取，如果没有则从数据库查询
            boolean needQueryUser = false;
            
            if (session.has("username")) {
                sessionInfo.setUsername(String.valueOf(session.get("username")));
            } else {
                needQueryUser = true;
            }
            
            if (session.has("nickname")) {
                sessionInfo.setNickname(String.valueOf(session.get("nickname")));
            } else {
                needQueryUser = true;
            }
            
            if (session.has("deptName")) {
                sessionInfo.setDeptName(String.valueOf(session.get("deptName")));
            }
            
            if (session.has("roleNames")) {
                Object roleNames = session.get("roleNames");
                if (roleNames instanceof List) {
                    sessionInfo.setRoleNames((List<String>) roleNames);
                }
            }
            
            // 如果session中没有用户信息，从数据库查询
            if (needQueryUser) {
                try {
                    User user = userService.getUserById(userId);
                    if (user != null) {
                        if (sessionInfo.getUsername() == null) {
                            sessionInfo.setUsername(user.getUsername());
                        }
                        if (sessionInfo.getNickname() == null) {
                            sessionInfo.setNickname(user.getNickname() != null ? user.getNickname() : user.getUsername());
                        }
                        if (sessionInfo.getDeptName() == null && user.getDeptId() != null) {
                            // 查询部门名称
                            try {
                                Dept dept = deptMapper.selectById(user.getDeptId());
                                if (dept != null) {
                                    sessionInfo.setDeptName(dept.getDeptName());
                                }
                            } catch (Exception e) {
                                logger.debug("查询部门信息失败: deptId={}", user.getDeptId());
                            }
                        }
                        if (sessionInfo.getRoleNames() == null || sessionInfo.getRoleNames().isEmpty()) {
                            // 查询用户角色名称
                            try {
                                List<String> roleIds = userService.getUserRoles(userId);
                                if (roleIds != null && !roleIds.isEmpty()) {
                                    // ✅ 批量查询角色名称（一条SQL搞定）
                                    try {
                                        List<Role> roles = roleMapper.selectBatchIds(roleIds);
                                        if (roles != null && !roles.isEmpty()) {
                                            List<String> roleNames = roles.stream()
                                                .map(Role::getName)
                                                .collect(java.util.stream.Collectors.toList());
                                            sessionInfo.setRoleNames(roleNames);
                                        }
                                    } catch (Exception e) {
                                        logger.debug("批量查询角色失败: roleIds={}", roleIds);
                                    }
                                }
                            } catch (Exception e) {
                                logger.debug("查询用户角色失败: userId={}", userId);
                            }
                        }
                        logger.debug("从数据库补充用户信息: userId={}, username={}, nickname={}", 
                                    userId, sessionInfo.getUsername(), sessionInfo.getNickname());
                    }
                } catch (Exception e) {
                    logger.warn("查询用户信息失败: userId={}, error={}", userId, e.getMessage());
                }
            }
                        
                        // 时间信息
                        sessionInfo.setCreateTime(LocalDateTime.now().minusSeconds((System.currentTimeMillis() - session.getCreateTime()) / 1000));
                        sessionInfo.setLastActiveTime(LocalDateTime.now());
                        
                        // Token有效期
            try {
                // 获取用户的token列表，取第一个token的超时时间
                List<String> tokenValues = StpUtil.getTokenValueListByLoginId(userId);
                if (tokenValues != null && !tokenValues.isEmpty()) {
                    String token = tokenValues.get(0);
                    // 使用 getTokenTimeout 获取token的剩余有效时间（秒）
                    long tokenTimeout = StpUtil.stpLogic.getTokenTimeout(token);
                    if (tokenTimeout != -1 && tokenTimeout != -2) {
                        // tokenTimeout: 剩余有效时间（秒）
                        // -1: 永不过期
                        // -2: token无效
                            sessionInfo.setTokenTimeout(tokenTimeout);
                        logger.debug("Token剩余时间: userId={}, remainingSeconds={}", userId, tokenTimeout);
                    } else if (tokenTimeout == -1) {
                        // 永不过期，设置一个很大的值（30天）
                        sessionInfo.setTokenTimeout(2592000L);
                        logger.debug("Token永不过期: userId={}", userId);
                    } else {
                        logger.debug("Token无效或已过期: userId={}", userId);
                    }
                }
            } catch (Exception e) {
                logger.debug("获取Token超时时间失败: userId={}, error={}", userId, e.getMessage());
                        }
                        
                        // 客户端信息
                        if (session.has("clientIp")) {
                            sessionInfo.setClientIp(String.valueOf(session.get("clientIp")));
                        }
                        if (session.has("loginLocation")) {
                            sessionInfo.setLoginLocation(String.valueOf(session.get("loginLocation")));
                        }
                        if (session.has("userAgent")) {
                            sessionInfo.setUserAgent(String.valueOf(session.get("userAgent")));
                        }
                        if (session.has("deviceInfo")) {
                            sessionInfo.setDeviceInfo(String.valueOf(session.get("deviceInfo")));
                        }
            if (session.has("deviceType")) {
                sessionInfo.setDeviceType(String.valueOf(session.get("deviceType")));
                    }
            if (session.has("osInfo")) {
                sessionInfo.setOsInfo(String.valueOf(session.get("osInfo")));
                }
            if (session.has("browser")) {
                sessionInfo.setBrowser(String.valueOf(session.get("browser")));
            }
            
            return sessionInfo;
        } catch (Exception e) {
            logger.error("构建SessionInfo失败: userId={}, error={}", userId, e.getMessage());
            return null;
        }
    }
    
    // ==================== 私有辅助方法 ====================
    
    /**
     * 验证用户状态
     * 
     * @param user 用户信息
     * @throws RuntimeException 用户状态异常
     */
    private void validateUserStatus(User user) {
        if (user == null) {
            throw BusinessException.authFailed("用户不存在");
        }
        
        if (!user.isEnabled()) {
            throw BusinessException.authFailed("用户账户已被禁用，请联系管理员");
        }
        
        if (user.isLocked() || user.isInLockPeriod()) {
            throw BusinessException.authFailed("用户账户已被锁定，请稍后再试或联系管理员");
        }
        
        // 🔧 修改：邮箱验证改为可选，只有当邮箱不为空且未验证时才检查
        // 注册时如果填写了邮箱，会要求验证；但登录不强制要求
        // 如果需要强制验证邮箱，可以取消下面的注释
        /*
        if (emailVerificationService != null 
                && user.getEmail() != null 
                && !user.getEmail().isEmpty() 
                && !Boolean.TRUE.equals(user.getEmailVerified())) {
            logger.warn("用户邮箱未验证: userId={}, email={}", user.getId(), user.getEmail());
            throw BusinessException.authFailed("邮箱未验证，请先验证邮箱后再登录");
        }
        */
        
        if (user.needChangePassword()) {
            // 密码过期不阻止登录，但需要提示
            logger.warn("用户密码已过期，建议尽快修改: userId={}", user.getId());
        }
    }
    
    /**
     * 从请求中提取Sa-Token令牌
     * 
     * @param request HTTP请求
     * @return Sa-Token令牌，如果不存在则返回null
     */
    private String extractTokenFromRequest(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        
        // 从 Authorization 头中提取 Bearer 令牌
        String authHeader = request.getHeader("Authorization");
        if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        
        // 从请求参数中提取
        String token = request.getParameter("access_token");
        if (StringUtils.hasText(token)) {
            return token;
        }
        
        return null;
    }

    /**
     * 从请求中提取 sessionId（优先从 Header: X-Session-Id）
     */
    private String extractSessionIdFromRequest(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        String sid = request.getHeader("X-Session-Id");
        if (StringUtils.hasText(sid)) {
            return sid;
        }
        return request.getParameter("session_id");
    }
    
    /**
     * 获取客户端IP地址
     * 
     * @param request HTTP请求
     * @return IP地址
     */
    // 已有 IpUtils.getClientIp(request) 全局工具，移除此重复方法
    
    /**
     * 获取客户端设备信息
     * 
     * @param request HTTP请求
     * @return 设备信息
     */
    private String getDeviceInfo(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        
        String userAgent = request.getHeader("User-Agent");
        if (!StringUtils.hasText(userAgent)) {
            return "unknown";
        }
        
        // 简单的设备信息提取，可以根据需要增强
        if (userAgent.toLowerCase().contains("mobile") || userAgent.toLowerCase().contains("android") || userAgent.toLowerCase().contains("iphone")) {
            return "mobile";
        } else if (userAgent.toLowerCase().contains("tablet") || userAgent.toLowerCase().contains("ipad")) {
            return "tablet";
        } else {
            return "desktop";
        }
    }
    
    /**
     * 获取访问令牌过期时间（秒）
     * 
     * @return 过期时间（秒）
     */
    private long getAccessTokenExpirationSeconds() {
        try {
            long seconds = securityProperties.getJwt().getAccessTokenExpire().toSeconds();
            return seconds > 0 ? seconds : 7200L;
        } catch (Exception e) {
            return 7200L;
        }
    }

    /**
     * 计算令牌的剩余存活秒数；若无法解析则返回默认值
     */
    private long computeRemainingTtlSeconds(String token, long defaultSeconds) {
        try {
            // Sa-Token架构下token验证和过期时间获取已简化
            // Sa-Token架构下使用内置token过期时间管理
            return defaultSeconds;
        } catch (Exception ignore) {}
        return defaultSeconds;
    }
    
    /**
     * 构建复合LoginId确保多租户权限缓存隔离
     * 
     * 格式：
     * - 租户模式开启：tenantId:userId
     * - 租户模式关闭：userId
     * 
     * @param tenantId 租户ID
     * @param userId 用户ID
     * @return 复合LoginId
     */
    private String buildCompositeLoginId(String tenantId, String userId) {
        // 检查租户功能是否启用
        if (tenantModeProperties != null && tenantModeProperties.isEnabled()) {
            // 多租户模式：使用复合ID确保权限缓存隔离
            return tenantId + ":" + userId;
        } else {
            // 单租户模式：直接使用用户ID
            return userId;
        }
    }
    
    /**
     * 检查是否启用并发控制
     */
    private boolean isConcurrentControlEnabled() {
        return securityProperties != null 
            && securityProperties.getSession() != null
            && securityProperties.getSession().isEnableConcurrentControl();
    }
    
    /**
     * 踢出之前的会话
     */
    private void kickoutPreviousSession(String tenantId, String userId) {
        String strategy = String.valueOf(securityProperties.getSession().getKickoutStrategy());
        if ("KICK_OUT_BEFORE".equalsIgnoreCase(strategy)) {
            String compositeLoginId = buildCompositeLoginId(tenantId, userId);
            StpUtil.kickout(compositeLoginId);
            logger.info("已踢出旧会话: userId={}, tenantId={}", userId, tenantId);
        }
    }
    
    // ==================== Sa-Token架构说明 ====================
    // 在Sa-Token架构下：
    // 1. 权限通过StpInterface动态获取，无需手动缓存
    // 2. Sa-Token内置Redis缓存机制，自动管理权限和角色缓存  
    // 3. 原有的手动缓存方法已移除，保持架构简洁
    // 4. 使用复合loginId确保多租户权限缓存隔离
    
    @Override
    public Long getOnlineUserCount() {
        try {
            List<SessionInfo> sessions = getAllActiveSessions();
            long count = sessions != null ? sessions.size() : 0L;
            logger.debug("获取全局在线用户数量: count={}", count);
            return count;
        } catch (Exception e) {
            logger.error("获取全局在线用户数量失败: error={}", e.getMessage());
            return 0L;
        }
    }
    
    @Override
    public boolean refreshUserPermissions(String userId) {
        try {
            String loginId = getLoginIdForUser(userId);
            
            if (!StpUtil.isLogin(loginId)) {
                logger.warn("用户未在线，无法刷新权限: userId={}", userId);
                return false;
            }
            
            String loginType = StpUtil.stpLogic.getLoginType();
            cn.dev33.satoken.dao.SaTokenDao dao = cn.dev33.satoken.SaManager.getSaTokenDao();
            dao.delete("satoken:" + loginType + ":permission:list:" + loginId);
            dao.delete("satoken:" + loginType + ":role:list:" + loginId);
            
            SaSession session = StpUtil.getSessionByLoginId(loginId, false);
            if (session != null) {
                UserAuthInfo freshUserInfo = userService.getUserAuthInfoById(userId);
                if (freshUserInfo != null) {
                    session.set("userInfo", freshUserInfo);
                }
                session.delete("permissions");
                session.delete("roles");
                session.delete("permissionCodes");
                session.delete("roleCodes");
            }
            
            try {
                userService.notifyUserPermissionChanged(userId, null, "管理员手动刷新权限");
            } catch (Exception e) {
                logger.debug("权限版本更新失败: {}", e.getMessage());
            }
            
            logger.info("用户权限刷新成功: userId={}", userId);
            return true;
            
        } catch (Exception e) {
            logger.error("刷新用户权限失败: userId={}, error={}", userId, e.getMessage(), e);
            return false;
        }
    }
    
    /**
     * 获取用户的完整loginId（包含租户ID）
     * 
     * @param userId 用户ID
     * @return 完整的loginId
     */
    private String getLoginIdForUser(String userId) {
        try {
            // 如果启用多租户，需要构造 tenantId:userId 格式
            if (tenantModeProperties != null && tenantModeProperties.isEnabled()) {
                User user = userService.getUserById(userId);
                if (user != null && user.getTenantId() != null) {
                    String loginId = user.getTenantId() + ":" + userId;
                    logger.debug("构造多租户loginId: userId={}, tenantId={}, loginId={}", 
                        userId, user.getTenantId(), loginId);
                    return loginId;
                }
            }
            logger.debug("使用单租户loginId: userId={}", userId);
            return userId;
        } catch (Exception e) {
            logger.warn("获取loginId失败，使用userId作为fallback: userId={}, error={}", 
                userId, e.getMessage());
            return userId;
        }
    }
    
    /**
     * 将Menu实体树转换为MenuInfo DTO树（递归）
     * 
     * @param menuTree Menu实体树
     * @return MenuInfo DTO树
     */
    private List<MenuInfo> convertMenuTreeToMenuInfo(List<Menu> menuTree) {
        if (menuTree == null || menuTree.isEmpty()) {
            return new ArrayList<>();
        }
        
        return menuTree.stream()
            .map(this::convertMenuToMenuInfo)
            .collect(java.util.stream.Collectors.toList());
    }
    
    /**
     * 将单个Menu实体转换为MenuInfo DTO（递归处理children）
     * 
     * @param menu Menu实体
     * @return MenuInfo DTO
     */
    private MenuInfo convertMenuToMenuInfo(Menu menu) {
        if (menu == null) {
            return null;
        }
        
        MenuInfo menuInfo = new MenuInfo();
        menuInfo.setId(menu.getId());
        menuInfo.setParentId(menu.getParentId());
        menuInfo.setName(menu.getName());
        menuInfo.setTitle(menu.getTitle());
        menuInfo.setPath(menu.getPath());
        menuInfo.setComponent(menu.getComponent());
        menuInfo.setIcon(menu.getIcon());
        menuInfo.setOrderNo(menu.getOrderNo());
        menuInfo.setMenuType(menu.getMenuType());
        menuInfo.setVisible(menu.getVisible());
        menuInfo.setStatus(menu.getStatus());
        menuInfo.setPermission(menu.getPermission());
        
        // 设置元数据
        if (menu.getTitle() != null || menu.getIcon() != null) {
            MenuInfo.MenuMeta meta = new MenuInfo.MenuMeta();
            meta.setTitle(menu.getTitle());
            meta.setIcon(menu.getIcon());
            menuInfo.setMeta(meta);
        }
        
        // 递归转换子菜单
        if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
            List<MenuInfo> children = menu.getChildren().stream()
                .map(this::convertMenuToMenuInfo)
                .collect(java.util.stream.Collectors.toList());
            menuInfo.setChildren(children);
        }
        
        return menuInfo;
    }
    
    /**
     * 统计菜单树的总菜单数（递归）
     * 
     * @param menuInfoTree MenuInfo树
     * @return 总菜单数
     */
    private int countTotalMenus(List<MenuInfo> menuInfoTree) {
        if (menuInfoTree == null || menuInfoTree.isEmpty()) {
            return 0;
        }
        
        int count = menuInfoTree.size();
        for (MenuInfo menuInfo : menuInfoTree) {
            if (menuInfo.getChildren() != null && !menuInfo.getChildren().isEmpty()) {
                count += countTotalMenus(menuInfo.getChildren());
            }
        }
        return count;
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void register(UserDTO userDTO, HttpServletRequest request) {
        ValidationUtils.notNull(userDTO, "注册请求不能为空");
        
        // 执行数据规范化
        userDTO.validate();
        
        String username = userDTO.getUsername();
        String password = userDTO.getPassword();
        String email = userDTO.getEmail();
        String emailCode = userDTO.getEmailCode();
        String nickname = userDTO.getNickname();
        String phone = userDTO.getPhone();
        String tenantId = userDTO.getTenantId();
        
        logger.info("用户注册请求: username={}, email={}, tenantId={}", username, email, tenantId);
        
        try {
            // 1. 密码一致性验证
            if (!userDTO.isPasswordMatch()) {
                throw BusinessException.invalidParam("confirmPassword", "两次输入的密码不一致");
            }
            
            // 2. 验证邮箱验证码
            if (emailVerificationService != null) {
                // 检查验证码是否为空
                if (!StringUtils.hasText(emailCode)) {
                    throw BusinessException.invalidParam("emailCode", "邮箱验证码不能为空");
                }
                
                // 检查验证码格式（必须是6位数字）
                if (!emailCode.matches("^\\d{6}$")) {
                    throw BusinessException.invalidParam("emailCode", "验证码必须是6位数字");
                }
                
                // 验证验证码
                boolean codeValid = emailVerificationService.verifyCode(email, emailCode);
                if (!codeValid) {
                    throw BusinessException.invalidParam("emailCode", "邮箱验证码错误或已过期");
                }
                logger.info("邮箱验证码验证成功: email={}", email);
            } else {
                logger.warn("邮箱验证服务未启用，跳过验证码验证");
            }
            
            // 3. 租户处理（与登录逻辑保持一致）
            if (securityProperties.getTenant().isEnabled()) {
                if (!StringUtils.hasText(tenantId)) {
                    if (securityProperties.getTenant().isAllowDefaultFallback()) {
                        tenantId = "default";
                        userDTO.setTenantId(tenantId);
                        logger.info("租户模式启用但未提供租户ID，使用默认租户: default");
                    } else {
                        throw BusinessException.authFailed("缺少租户标识");
                    }
                }
            } else {
                tenantId = null;
                userDTO.setTenantId(null);
                logger.debug("租户模式已关闭，tenantId设为null");
            }
            
            // 3. 创建新用户实体
            User newUser = new User();
            newUser.setTenantId(tenantId);
            newUser.setUsername(username);
            newUser.setPassword(password); // 密码会在UserService中自动加密
            newUser.setNickname(nickname);
            newUser.setEmail(email);
            newUser.setPhone(phone);
            newUser.setStatus(1); // 启用状态
            newUser.setIsLocked(false);
            
            // 4. 设置密码过期时间
            SecurityProperties.Security.Password passwordConfig = securityProperties.getSecurity().getPassword();
            if (passwordConfig != null && passwordConfig.getExpireDays() > 0) {
                newUser.setPasswordExpireAt(
                    LocalDateTime.now().plusDays(passwordConfig.getExpireDays())
                );
            }
            
            // 5. 设置客户端信息
            String clientIp = IpUtils.getClientIp(request);
            String userAgent = request != null ? request.getHeader("User-Agent") : null;
            newUser.setLastLoginIp(clientIp);
            
            // 6. 保存用户（自动加密密码、设置默认值、检查唯一性）
            User createdUser = userService.createUser(newUser);
            if (createdUser == null) {
                throw BusinessException.operationFailed("用户注册失败，请稍后再试");
            }
            
            logger.info("用户注册成功: userId={}, username={}, email={}, tenantId={}", 
                       createdUser.getId(), username, email, tenantId);
            
            // 7. 记录注册日志
            try {
                if (loginLogMapper != null) {
                    LoginLog log = new LoginLog();
                    log.setTenantId(StringUtils.hasText(tenantId) ? tenantId : "default");
                    log.setUsername(username);
                    log.setIp(clientIp);
                    log.setLocation(getLocationByIp(clientIp));
                    log.setUserAgent(userAgent);
                    log.setStatus("REGISTER");
                    log.setMessage("用户注册成功");
                    log.setCreateTime(LocalDateTime.now());
                    loginLogMapper.insert(log);
                }
            } catch (Exception e) {
                logger.debug("记录注册日志失败: {}", e.getMessage());
            }
            
            // 8. 注册成功（邮箱已通过验证码验证）
            logger.info("用户注册流程完成: userId={}, username={}, emailVerified=true", 
                       createdUser.getId(), username);
            
        } catch (BusinessException e) {
            logger.warn("用户注册失败: username={}, email={}, error={}", username, email, e.getMessage());
            
            // 记录注册失败日志
            try {
                if (loginLogMapper != null) {
                    LoginLog log = new LoginLog();
                    log.setTenantId(StringUtils.hasText(tenantId) ? tenantId : "default");
                    log.setUsername(username);
                    log.setIp(IpUtils.getClientIp(request));
                    log.setUserAgent(request != null ? request.getHeader("User-Agent") : null);
                    log.setStatus("REGISTER_FAILURE");
                    log.setMessage(e.getMessage());
                    log.setCreateTime(LocalDateTime.now());
                    loginLogMapper.insert(log);
                }
            } catch (Exception ex) {
                logger.debug("记录注册失败日志失败: {}", ex.getMessage());
            }
            
            throw e;
        } catch (Exception e) {
            logger.error("用户注册异常: username={}, email={}, error={}", 
                        username, email, e.getMessage(), e);
            
            // 记录注册异常日志
            try {
                if (loginLogMapper != null) {
                    LoginLog log = new LoginLog();
                    log.setTenantId(StringUtils.hasText(tenantId) ? tenantId : "default");
                    log.setUsername(username);
                    log.setIp(IpUtils.getClientIp(request));
                    log.setUserAgent(request != null ? request.getHeader("User-Agent") : null);
                    log.setStatus("REGISTER_ERROR");
                    log.setMessage("系统异常");
                    log.setCreateTime(LocalDateTime.now());
                    loginLogMapper.insert(log);
                }
            } catch (Exception ex) {
                logger.debug("记录注册异常日志失败: {}", ex.getMessage());
            }
            
            throw BusinessException.systemError("用户注册失败，请稍后再试");
        }
    }
}
